﻿/******************************************************************************************************
Copyright (c) 2014 Steven Giacomelli (stevegiacomelli@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this 
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, 
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 
permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies 
or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************************************/

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace SharpGeo
{
    /// <summary>
    /// A map projection superclass with an underlying CoordinateReferenceSystem
    /// </summary>
    public abstract class Projection : CoordinateReferenceSystem
    {
        #region Constructor
        /// <summary>
        /// Constructs a Projection
        /// </summary>
        /// <param name="crs">The underlying CoordinateReferenceSystem</param>
        protected Projection(CoordinateReferenceSystem crs)
        {
            this.CoordinateReferenceSystem = crs;
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets the Minimum Projected Latitude
        /// </summary>
        public virtual double MinLatitude { get { return -90d; } }
        /// <summary>
        /// Gets the Maximum Projected Latitude
        /// </summary>
        public virtual double MaxLatitude { get { return 90d; } }
        /// <summary>
        /// Gets the Minimum Projected Longitude
        /// </summary>
        public virtual double MinLongitude { get { return -180d; } }
        /// <summary>
        /// Gets the Maximum Projected Longitude
        /// </summary>
        public virtual double MaxLongitude { get { return 180d; } }
        /// <summary>
        /// Gets the underlying CoordinateRefenceSystem
        /// </summary>
        public CoordinateReferenceSystem CoordinateReferenceSystem { get; protected set; }
        #endregion

        #region Methods
        #region Abstract Methods
        /// <summary>
        /// Projects a ICoordinate to the Screen (X,Y) using the implemented Projection
        /// </summary>
        /// <param name="coordinate">The ICoordinate to project</param>
        /// <param name="mapWidth">The map width</param>
        /// <param name="mapHeight">The map height</param>
        /// <param name="transformation">The transformation to apply post projection</param>
        /// <returns>The projected ICoordinate</returns>
        public abstract ICoordinate ProjectToScreen(
            ICoordinate coordinate,
            double mapWidth,
            double mapHeight,
            ICoordinateTransformation transformation);

        /// <summary>
        /// Inverse projects an ICoordinate from the screen back to the underlying Coordinate Reference System
        /// </summary>
        /// <param name="coordinate">The ICoordinate to inverse project</param>
        /// <param name="mapWidth">The map width</param>
        /// <param name="mapHeight">The map height</param>
        /// <param name="transformation">The transformation to inverse apply pre inverse projection</param>
        /// <returns>The inverse projected ICoordinate</returns>
        public abstract ICoordinate InverseProjectFromScreen(
            ICoordinate coordinate,
            double mapWidth,
            double mapHeight,
        ICoordinateTransformation transformation);
        #endregion

        #region Bulk Methods
        /// <summary>
        /// Projects a IReadOnlyCoordinateCollection to screen coordinates X,Y)
        /// </summary>
        /// <param name="coordinates">The coordinates to project</param>
        /// <param name="mapWidth">The map width</param>
        /// <param name="mapHeight">The map height</param>
        /// <returns>The projected coordinates</returns>
        public ICoordinate[] ProjectPointsToScreen(
            IReadOnlyCoordinateCollection coordinates,
            double mapWidth,
            double mapHeight)
        {
            return ProjectPointsToScreen(coordinates, mapWidth, mapHeight, null);
        }

        /// <summary>
        /// Projects a IReadOnlyCoordinateCollection to screen coordinates X,Y)
        /// </summary>
        /// <param name="coordinates">The coordinates to project</param>
        /// <param name="mapWidth">The map width</param>
        /// <param name="mapHeight">The map height</param>
        /// <param name="transformation">The transformation to apply post projection</param>
        /// <returns>The projected coordinates</returns>
        public ICoordinate[] ProjectPointsToScreen(
            IReadOnlyCoordinateCollection coordinates,
           double mapWidth,
            double mapHeight,
           ICoordinateTransformation transformation)
        {
            if (coordinates == null)
                return new ICoordinate[0];

            var points = new ICoordinate[coordinates.Count];

            if (points.Length < Environment.ProcessorCount)
            {
                for (int i = 0; i < points.Length; i++)
                {
                    points[i] = ProjectToScreen(coordinates.GetValue(i), mapWidth, mapHeight, transformation);
                }
            }
            else
            {
                Parallel.For(0, points.Length, i =>
                {
                    points[i] = ProjectToScreen(coordinates.GetValue(i), mapWidth, mapHeight, transformation);
                });
            }
            return points;
        }

        /// <summary>
        /// Projects an Envelope to screen coordinates (X,Y)
        /// </summary>
        /// <param name="envelope">The envelope to project</param>
        /// <param name="mapWidth">The map width</param>
        /// <param name="mapHeight">The map height</param>
        /// <returns>The projected Envelope</returns>
        public Envelope ProjectEnvelopeToScreen(
           Envelope envelope,
           double mapWidth, 
            double mapHeight)
        {
            return ProjectEnvelopeToScreen(envelope, mapWidth, mapHeight, null);
        }

        /// <summary>
        /// Projects an Envelope to screen coordinates (X,Y)
        /// </summary>
        /// <param name="envelope">The envelope to project</param>
        /// <param name="mapWidth">The map width</param>
        /// <param name="mapHeight">The map height</param>
        /// <param name="transformation">The transformation to apply post projection</param>
        /// <returns>The projected Envelope</returns>
        public Envelope ProjectEnvelopeToScreen(
            Envelope envelope,
            double mapWidth, double mapHeight,
            ICoordinateTransformation transformation)
        {
            var p1 = ProjectToScreen(new GeographicCoordinate(envelope.MinX, envelope.MaxY), mapWidth, mapHeight, transformation);
            var p2 = ProjectToScreen(new GeographicCoordinate(envelope.MaxX, envelope.MinY), mapWidth, mapHeight, transformation);

            return new Envelope(p1[0], p1[1], p2[0], p2[1]);
        }
        #endregion

        #region Helper Overloads
        /// <summary>
        /// Projects a ICoordinate to the Screen (X,Y) using the implemented Projection
        /// </summary>
        /// <param name="coordinate">The ICoordinate to project</param>
        /// <param name="mapWidth">The map width</param>
        /// <param name="mapHeight">The map height</param>
        /// <returns>The projected ICoordinate</returns>
        public ICoordinate ProjectToScreen(ICoordinate coordinate, double mapWidth, double mapHeight)
        {
            return ProjectToScreen(coordinate, mapWidth, mapHeight, null);
        }

        /// <summary>
        /// Inverse projects an ICoordinate from the screen back to the underlying Coordinate Reference System
        /// </summary>
        /// <param name="coordinate">The ICoordinate to inverse project</param>
        /// <param name="mapWidth">The map width</param>
        /// <param name="mapHeight">The map height</param>
        /// <returns>The inverse projected ICoordinate</returns>
        public ICoordinate InverseProjectFromScreen(ICoordinate coordinate, double mapWidth, double mapHeight)
        {
            return InverseProjectFromScreen(coordinate, mapWidth, mapHeight, null);
        }

        #endregion

        #region Distance Calculation
        /// <summary>
        /// Calculates the distance between two coordinates using the underlying CoordinateReferenceSystem
        /// </summary>
        /// <param name="coordinate1">The first ICoordinate</param>
        /// <param name="coordinate2">The second ICoordinate</param>
        /// <returns>The distance in meters</returns>
        public override double CalculateDistance(ICoordinate coordinate1, ICoordinate coordinate2)
        {
            //this method(s) are misleading, it would seem to imply calculating 
            //the distance in linear units based on the projection
            //this will be rectified in future versions
            return CoordinateReferenceSystem.CalculateDistance(coordinate1, coordinate2);
        }

        /// <summary>
        /// Calculates the distance between two coordinates using the underlying CoordinateReferenceSystem
        /// </summary>
        /// <param name="longitude1">The first longitude</param>
        /// <param name="latitude1">The first latitude</param>
        /// <param name="longitude2">The second longitude</param>
        /// <param name="latitude2">The second latitude</param>
        /// <returns>The distance in meters</returns>
        public virtual double CalculateDistance(
            double longitude1,
            double latitude1,
            double longitude2,
            double latitude2)
        {
            return CoordinateReferenceSystem.CalculateDistance(
                new GeographicCoordinate(longitude1, latitude1),
                new GeographicCoordinate(longitude2, latitude2));
        }
        #endregion

        #endregion
    }
}